package com.google.code.dnaep.jdo;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;

import javax.jdo.FetchPlan;
import javax.jdo.PersistenceManager;

import org.datanucleus.ObjectManager;
import org.datanucleus.exceptions.NucleusException;
import org.datanucleus.jdo.JDOExtent;
import org.datanucleus.jdo.JDOPersistenceManager;
import org.datanucleus.jdo.JDOQuery;
import org.datanucleus.jdo.NucleusJDOHelper;

import com.google.code.dnaep.EntityKindHelper;

public class Query implements javax.jdo.Query {

	private final JDOQuery jdoQuery;
	private final Collection<JDOQuery> subclassJdoQueries = Collections.synchronizedCollection(new HashSet<JDOQuery>());
	private Extent gaeExtent;

	public Query(JDOQuery jdoQuery) {
		this.jdoQuery = jdoQuery;
	}

	@Override
	public void close(Object queryResult) {
		jdoQuery.close(queryResult);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.close(queryResult);
		}
	}

	@Override
	public void closeAll() {
		jdoQuery.closeAll();
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.closeAll();
		}
	}

	@Override
	public void compile() {
		ensureSuclassJdoQueries(false);
		jdoQuery.compile();
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.compile();
		}
	}

	@Override
	public void declareImports(String imports) {
		jdoQuery.declareImports(imports);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.declareImports(imports);
		}
	}

	@Override
	public void declareParameters(String parameters) {
		jdoQuery.declareParameters(parameters);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.declareParameters(parameters);
		}
	}

	@Override
	public void declareVariables(String variables) {
		jdoQuery.declareVariables(variables);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.declareVariables(variables);
		}
	}

	@Override
	public long deletePersistentAll() {
		ensureSuclassJdoQueries(false);
		long deleted = jdoQuery.deletePersistentAll();
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			deleted += subclassJdoQuery.deletePersistentAll();
		}
		return deleted;
	}

	@Override
	public long deletePersistentAll(Object... parameters) {
		ensureSuclassJdoQueries(false);
		long deleted = jdoQuery.deletePersistentAll(parameters);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			deleted += subclassJdoQuery.deletePersistentAll(parameters);
		}
		return deleted;
	}

	@Override
	public long deletePersistentAll(Map parameters) {
		ensureSuclassJdoQueries(false);
		long deleted = jdoQuery.deletePersistentAll(parameters);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			deleted += subclassJdoQuery.deletePersistentAll(parameters);
		}
		return deleted;
	}

	@Override
	public Object execute() {
		return executeWithArray(new Object[] {});
	}

	@Override
	public Object execute(Object p1) {
		return executeWithArray(new Object[] { p1 });
	}

	@Override
	public Object execute(Object p1, Object p2) {
		return executeWithArray(new Object[] { p1, p2 });
	}

	@Override
	public Object execute(Object p1, Object p2, Object p3) {
		return executeWithArray(new Object[] { p1, p2, p3 });
	}

	@Override
	public Object executeWithArray(Object... parameterValues) {
		ensureSuclassJdoQueries(false);
		
		//TODO special collection that preserves the LazyLoad
		Collection<Object> results = new ArrayList<Object>();
		Object result = jdoQuery.executeWithArray(parameterValues);
		if (result instanceof Collection) {
			results.addAll((Collection<Object>) result);
		} else {
			results.add(result);
		}
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			result = subclassJdoQuery.executeWithArray(parameterValues);
			if (result instanceof Collection) {
				results.addAll((Collection<Object>) result);
			} else {
				results.add(result);
			}
		}
		return results;
	}

	@Override
	public Object executeWithMap(Map parameters) {
		ensureSuclassJdoQueries(false);

		//TODO special collection that preserves the LazyLoad
		Collection<Object> results = new ArrayList<Object>();
		Object result = jdoQuery.executeWithMap(parameters);
		if (result instanceof Collection) {
			results.addAll((Collection<Object>) result);
		} else {
			results.add(result);
		}
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			result = subclassJdoQuery.executeWithMap(parameters);
			if (result instanceof Collection) {
				results.addAll((Collection<Object>) result);
			} else {
				results.add(result);
			}
		}
		return results;
	}

	@Override
	public void cancelAll() {
		jdoQuery.cancelAll();
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.cancelAll();
		}
	}

	@Override
	public void cancel(Thread thr) {
		jdoQuery.cancel(thr);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.cancel(thr);
		}
	}

	@Override
	public void setCandidates(javax.jdo.Extent extent) {
		Extent gaeExtent = (Extent) extent;

		JDOExtent jdoExtent = gaeExtent.getJdoExtent();
		jdoQuery.setCandidates(jdoExtent);

		this.gaeExtent = gaeExtent;
		ensureSuclassJdoQueries(true);

		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.setCandidates(jdoExtent);
		}
	}

	@Override
	public void setCandidates(Collection pcs) {
		jdoQuery.setCandidates(pcs);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.setCandidates(pcs);
		}
	}

	@Override
	public void setClass(Class candidateClass) {
		jdoQuery.setClass(candidateClass);
		ensureSuclassJdoQueries(true);
	}

	@Override
	public void addExtension(String key, Object value) {
		jdoQuery.addExtension(key, value);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.addExtension(key, value);
		}
	}

	@Override
	public void setExtensions(Map extensions) {
		jdoQuery.setExtensions(extensions);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.setExtensions(extensions);
		}
	}

	@Override
	public FetchPlan getFetchPlan() {
		return jdoQuery.getFetchPlan();
	}

	@Override
	public void setFilter(String filter) {
		jdoQuery.setFilter(filter);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.setFilter(filter);
		}
	}

	@Override
	public void setGrouping(String grouping) {
		throw new RuntimeException("Not supported");
		// TODO grouping needs special handling when execution
//		jdoQuery.setGrouping(grouping);
//		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
//			subclassJdoQuery.setGrouping(grouping);
//		}
	}

	@Override
	public boolean getIgnoreCache() {
		return jdoQuery.getIgnoreCache();
	}

	@Override
	public void setIgnoreCache(boolean ignoreCache) {
		jdoQuery.setIgnoreCache(ignoreCache);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.setIgnoreCache(ignoreCache);
		}
	}

	@Override
	public void setOrdering(String ordering) {
		throw new RuntimeException("Not supported");
		// TODO ordering needs special handling when execution
		// jdoQuery.setOrdering(ordering);
		// for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
		// subclassJdoQuery.setOrdering(ordering);
		// }
	}

	@Override
	public PersistenceManager getPersistenceManager() {
		return jdoQuery.getPersistenceManager();
	}

	@Override
	public void setRange(String range) {
		throw new RuntimeException("Not supported");
		// TODO range needs special handling when execution
//		jdoQuery.setRange(range);
//		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
//			subclassJdoQuery.setRange(range);
//		}
	}

	@Override
	public void setRange(long fromIncl, long toExcl) {
		throw new RuntimeException("Not supported");
		// TODO range needs special handling when execution
//		jdoQuery.setRange(fromIncl, toExcl);
//		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
//			subclassJdoQuery.setRange(fromIncl, toExcl);
//		}
	}

	@Override
	public void setResult(String result) {
		jdoQuery.setResult(result);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.setResult(result);
		}
	}

	@Override
	public void setResultClass(Class result_cls) {
		jdoQuery.setResultClass(result_cls);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.setResultClass(result_cls);
		}
	}

	@Override
	public void setTimeoutMillis(Integer timeout) {
		// TODO timeout might need special handling as we will perform multiple
		// queries
		jdoQuery.setTimeoutMillis(timeout);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.setTimeoutMillis(timeout);
		}
	}

	@Override
	public Integer getTimeoutMillis() {
		// TODO timeout might need special handling as we will perform multiple
		// queries
		return jdoQuery.getTimeoutMillis();
	}

	@Override
	public void setUnique(boolean unique) {
		throw new RuntimeException("Not supported");
		// TODO range needs special handling when execution
		// jdoQuery.setUnique(unique);
		// for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
		// subclassJdoQuery.setUnique(unique);
		// }
	}

	@Override
	public boolean isUnmodifiable() {
		return jdoQuery.isUnmodifiable();
	}

	@Override
	public void setUnmodifiable() {
		jdoQuery.setUnmodifiable();
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.setUnmodifiable();
		}
	}

	@Override
	public void addSubquery(javax.jdo.Query sub, String variableDecl, String candidateExpr) {
		jdoQuery.addSubquery(sub, variableDecl, candidateExpr);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.addSubquery(sub, variableDecl, candidateExpr);
		}
	}

	@Override
	public void addSubquery(javax.jdo.Query sub, String variableDecl, String candidateExpr, String parameter) {
		jdoQuery.addSubquery(sub, variableDecl, candidateExpr, parameter);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.addSubquery(sub, variableDecl, candidateExpr, parameter);
		}
	}

	@Override
	public void addSubquery(javax.jdo.Query sub, String variableDecl, String candidateExpr, String... parameters) {
		jdoQuery.addSubquery(sub, variableDecl, candidateExpr, parameters);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.addSubquery(sub, variableDecl, candidateExpr, parameters);
		}
	}

	@Override
	public void addSubquery(javax.jdo.Query sub, String variableDecl, String candidateExpr, Map parameters) {
		jdoQuery.addSubquery(sub, variableDecl, candidateExpr, parameters);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.addSubquery(sub, variableDecl, candidateExpr, parameters);
		}
	}

	@Override
	public Boolean getSerializeRead() {
		return jdoQuery.getSerializeRead();
	}

	@Override
	public void setSerializeRead(Boolean serialize) {
		jdoQuery.setSerializeRead(serialize);
		for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
			subclassJdoQuery.setSerializeRead(serialize);
		}
	}

	@Override
	public String toString() {
		return "GAEQuery[\"" + jdoQuery.toString() + "\", isSubclasses: " + jdoQuery.getInternalQuery().isSubclasses();
	}

	protected synchronized void ensureSuclassJdoQueries(boolean regenerate) {
		if (!subclassJdoQueries.isEmpty() && !regenerate) {
			return;
		}
		synchronized (subclassJdoQueries) {
			if (regenerate) {
				for (JDOQuery subclassJdoQuery : subclassJdoQueries) {
					subclassJdoQuery.closeAll();
				}
				subclassJdoQueries.clear();
			}
			if (!includeSubclasses()) {
				return;
			}
			Class parentClass = jdoQuery.getInternalQuery().getCandidateClass();
			Collection<Class> subclasses = EntityKindHelper.getSubclasses(parentClass);
			for (Class subclass : subclasses) {
				JDOQuery subclassQuery = (JDOQuery) cloneJDOQuery(jdoQuery);
				subclassQuery.setClass(subclass);
				subclassJdoQueries.add(subclassQuery);
			}
		}
	}

	private boolean includeSubclasses() {
		if (gaeExtent != null) {
			return gaeExtent.hasSubclasses();
		}
		return true;
	}

	protected JDOQuery cloneJDOQuery(JDOQuery jdoQuery) {
		String queryLanguage = jdoQuery.getLanguage();
		org.datanucleus.store.query.Query oldInternalQuery = jdoQuery.getInternalQuery();
		JDOPersistenceManager pm = (JDOPersistenceManager) jdoQuery.getPersistenceManager();
		ObjectManager objectMgr = pm.getObjectManager();

		org.datanucleus.store.query.Query newInternalQuery = null;
		try {
			// Extract the internal query for generating the next query
			newInternalQuery = objectMgr.getOMFContext().getQueryManager().newQuery(queryLanguage, objectMgr, oldInternalQuery);
			// TODO verify that everything is copied over
			// TODO copy over candidates both collection and extent
		} catch (NucleusException jpe) {
			throw NucleusJDOHelper.getJDOExceptionForNucleusException(jpe);
		}
		return new JDOQuery(pm, newInternalQuery, queryLanguage);
	}

}
